package com.xuecheng.media.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.xuecheng.base.exception.XueChengPlusException;
import com.xuecheng.base.model.PageParams;
import com.xuecheng.base.model.PageResult;
import com.xuecheng.base.model.RestResponse;
import com.xuecheng.media.mapper.MediaFilesMapper;
import com.xuecheng.media.model.dto.QueryMediaParamsDto;
import com.xuecheng.media.model.dto.UploadFileParamsDto;
import com.xuecheng.media.model.dto.UploadFileResultDto;
import com.xuecheng.media.model.po.MediaFiles;
import com.xuecheng.media.service.MediaFileService;
import io.minio.*;
import io.minio.errors.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.text.SimpleDateFormat;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.List;

/**
 * @description TODO
 * @author Mr.M
 * @date 2022/9/10 8:58
 * @version 1.0
 */
@Slf4j
 @Service
public class MediaFileServiceImpl implements MediaFileService {

  @Autowired
 MediaFilesMapper mediaFilesMapper;

  @Autowired
 MinioClient minioClient;

  @Autowired
  MediaFileService currentProxy;
 //普通文件存储的桶
  @Value("${minio.bucket.files}")
  private String bucket_files;
 //视频文件存储的桶
 @Value("${minio.bucket.videofiles}")
 private String bucket_videofiles;

 @Override
 public PageResult<MediaFiles> queryMediaFiels(Long companyId,PageParams pageParams, QueryMediaParamsDto queryMediaParamsDto) {

  //构建查询条件对象
  LambdaQueryWrapper<MediaFiles> queryWrapper = new LambdaQueryWrapper<>();
  queryWrapper.like(StringUtils.isNotBlank(queryMediaParamsDto.getFilename()),MediaFiles::getFilename,queryMediaParamsDto.getFilename());
  queryWrapper.eq(StringUtils.isNotBlank(queryMediaParamsDto.getFileType()),MediaFiles::getFileType,queryMediaParamsDto.getFileType());
  queryWrapper.eq(StringUtils.isNotBlank(queryMediaParamsDto.getAuditStatus()),MediaFiles::getAuditStatus,"002003");
  
  //分页对象
  Page<MediaFiles> page = new Page<>(pageParams.getPageNo(), pageParams.getPageSize());
  // 查询数据内容获得结果
  Page<MediaFiles> pageResult = mediaFilesMapper.selectPage(page, queryWrapper);
  // 获取数据列表
  List<MediaFiles> list = pageResult.getRecords();
  // 获取数据总数
  long total = pageResult.getTotal();
  // 构建结果集
  PageResult<MediaFiles> mediaListResult = new PageResult<>(list, total, pageParams.getPageNo(), pageParams.getPageSize());
  return mediaListResult;

 }

 //文件上传的通用接口
 @Override
 public UploadFileResultDto uploadFile(Long companyId, UploadFileParamsDto dto, byte[] bytes, String folder, String objectName) {

  //得到文件的md5值
  String fileMd5 = DigestUtils.md5Hex(bytes);
  if (StringUtils.isEmpty(folder)){
   //自动生成目录的路径，按年月日生成
   folder = getFileFolder(new Date(), true, true, true);
  }else if (folder.indexOf("/") < 0){
   folder = folder + "/";
  }
  String filename = dto.getFilename();
  if (StringUtils.isEmpty(objectName)){
   //如果objectName为空，我们使用文件的md5值为objectName
   objectName = fileMd5 + filename.substring(filename.lastIndexOf("."));
  }
  objectName = folder + objectName;
  try {
   //上传到分布式文件系统
   addMediaFilesToMinIO(bytes,bucket_files,objectName);
   //上传到数据库
   MediaFiles mediaFiles = currentProxy.addMedisFilesToDb(companyId, fileMd5, dto, bucket_files, objectName);
   //准备返回数据
   UploadFileResultDto uploadFileResultDto = new UploadFileResultDto();
   BeanUtils.copyProperties(mediaFiles,uploadFileResultDto);
   return uploadFileResultDto;
  } catch (Exception e) {
   log.debug("上传文件失败:{}",e.getMessage());
   //处理掉异常之后要抛出去，为了让Spring知道，并完成事务控制
    throw new RuntimeException(e.getMessage());
  }
 }
 //将文件上传到分布式文件系统
 private void addMediaFilesToMinIO(String filePath,String bucket_files,String objectName){
  try {
   UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
           .bucket(bucket_files)
           .object(objectName)
           .filename(filePath)
           .build();
   minioClient.uploadObject(uploadObjectArgs);
   log.debug("将合并后的文件上传成功:{}",filePath);
  } catch (Exception e) {
   e.printStackTrace();
   XueChengPlusException.cast("上传失败！");
  }
 }
 //将文件上传到分布式文件系统
 private void addMediaFilesToMinIO(byte[] bytes ,String bucket_files,String objectName){

  //资源的媒体类型
  String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; //默认的二进制流
  if (objectName.indexOf(".") >= 0){
   //取objectName当中的扩展名
   String extension = objectName.substring(objectName.lastIndexOf("."));
   ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
   if (extensionMatch != null){
    contentType = extensionMatch.getMimeType();
   }
  }

  //上传到minio
  try {
   //为什么要将file转为字节数组的方式来上传呢？就是为了解耦合，为了代码更具有通用性
   ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);
   PutObjectArgs build = PutObjectArgs.builder()
           .bucket(bucket_files)
           .object(objectName)
           //InputStream stream, long objectSize 对象大小, long partSize 分片大小(-1表示最小分片：5M)
           .stream(byteArrayInputStream, byteArrayInputStream.available(), -1)
           .contentType(contentType)
           .build();
   minioClient.putObject(build);
  } catch (Exception e){
   e.printStackTrace();
   log.debug("上传文件失败：{}",e.getMessage());
   XueChengPlusException.cast("上传文件失败");
  }
 }

 @Transactional
 //将文件上传到数据库
 public MediaFiles addMedisFilesToDb(Long companyId,String fileId,UploadFileParamsDto dto,String bucket_files,String objectName) {
  //上传到数据库
  MediaFiles mediaFiles = mediaFilesMapper.selectById(fileId);
  if (mediaFiles == null) {

   mediaFiles = new MediaFiles();

   //封装数据
   BeanUtils.copyProperties(dto, mediaFiles);
   mediaFiles.setId(fileId);
   mediaFiles.setFileId(fileId);
   mediaFiles.setCompanyId(companyId);
   mediaFiles.setBucket(bucket_files);
   mediaFiles.setFilePath(objectName);

   //获取扩展名
   String extension = null;
   String filename = dto.getFilename();
   if (StringUtils.isNotEmpty(filename) && filename.indexOf(".") >= 0){
    extension = filename.substring(filename.lastIndexOf("."));
   }
   //媒体类型
   String mimeTypeByExtension = getMimeTypeByExtension(extension);
   //图片、MP4视频可以设置URL
  if (mimeTypeByExtension.indexOf("image") >= 0 || mimeTypeByExtension.indexOf("mp4") >= 0){
   mediaFiles.setUrl("/" + bucket_files + "/" + objectName);
  }
   mediaFiles.setCreateDate(LocalDateTime.now());
   mediaFiles.setStatus("1");
   mediaFiles.setAuditStatus("002003");
   //插入文件表
   mediaFilesMapper.insert(mediaFiles);
  }
  return mediaFiles;
 }

 //获取扩展名：根据扩展名来取匹配的媒体类型
 private String getMimeTypeByExtension(String extension){
  //资源的媒体类型
  String contentType = MediaType.APPLICATION_OCTET_STREAM_VALUE; //默认的二进制流
  if (StringUtils.isNotEmpty(extension)){
   ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
   if (extensionMatch != null){
    contentType = extensionMatch.getMimeType();
   }
  }
  return contentType;
 }

 //根据日期拼接目录
 private String getFileFolder(Date date, boolean year, boolean month, boolean day){
  SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  //获取当前日期字符串
  String dateString = sdf.format(date);
  //取出年、月、日
  String[] dateStringArray = dateString.split("-");
  StringBuffer folderString = new StringBuffer();
  if(year){
   folderString.append(dateStringArray[0]);
   folderString.append("/");
  }
  if(month){
   folderString.append(dateStringArray[1]);
   folderString.append("/");
  }
  if(day){
   folderString.append(dateStringArray[2]);
   folderString.append("/");
  }

  return folderString.toString();
 }

 //检查文件是否存在
 @Override
 public RestResponse<Boolean> checkFile(String fileMd5) {

  //在数据库文件表中存在，并且在文件系统中也存在，此文件才存在
  MediaFiles mediaFiles = mediaFilesMapper.selectById(fileMd5);
  if (mediaFiles == null){
   return RestResponse.success(false); //不存在
  }
  //查看是否在文件系统中存在
  GetObjectArgs build = GetObjectArgs.builder().bucket(mediaFiles.getBucket()).object(mediaFiles.getFilePath()).build();
  try {
   FilterInputStream inputStream = minioClient.getObject(build);
   if (inputStream == null){
    return RestResponse.success(false); //不存在
   }
  } catch (Exception e) {
   e.printStackTrace();
   return RestResponse.success(false); //不存在
  }
  //文件已经存在
  return RestResponse.success(true);
 }

 //检查分块是否存在
 @Override
 public RestResponse<Boolean> checkChunk(String fileMd5, int chunkIndex) {

  //得到分块文件所在目录
  String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
  //分块文件的路径
  String chunkFilePath = chunkFileFolderPath + chunkIndex;
  //查询文件系统：具体分块文件是否存在
  GetObjectArgs build = GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build();
  try {
   FilterInputStream inputStream = minioClient.getObject(build);
   if (inputStream == null){
    return RestResponse.success(false); //不存在
   }
  } catch (Exception e) {
   e.printStackTrace();
   return RestResponse.success(false); //不存在
  }
  //文件已经存在
  return RestResponse.success(true);
 }

 //得到分块文件的目录
 private String getChunkFileFolderPath(String fileMd5) {
  return fileMd5.substring(0, 1) + "/" + fileMd5.substring(1, 2) + "/" + fileMd5 + "/" + "chunk" + "/";
 }

 @Override
 public RestResponse uploadChunk(String fileMd5, int chunk, byte[] bytes) {

  //得到分块文件的目录路径
  String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
  //得到分块文件的路径
  String chunkFilePath = chunkFileFolderPath + chunk;

  try {
   //将分块上传到minio
   addMediaFilesToMinIO(bytes,bucket_videofiles,chunkFilePath);
   return RestResponse.success(true);
  }catch (Exception e){
   //文件上传失败
   return RestResponse.validfail(false,"上传分块失败");
  }

 }

 //合并分块
 @Override
 public RestResponse mergechunks(Long companyId, String fileMd5, int chunkTotal, UploadFileParamsDto uploadFileParamsDto) {

  //下载分块
  File[] files = checkChunkStatus(fileMd5, chunkTotal);

  //得到合并后文件的扩展名
  String filename = uploadFileParamsDto.getFilename();
  String extension = filename.substring(filename.lastIndexOf("."));
  //创建一个临时文件，作为合并文件
  File merge = null;
  try {
   try {
    merge = File.createTempFile("merge", extension);
   } catch (Exception e){
    XueChengPlusException.cast("创建临时文件出错！");
   }

   //合并分块
   //建立缓冲区
   byte[] bytes = new byte[1024];
   int len;
   //创建合并文件的流对象
   FileOutputStream write = null;
   FileInputStream read = null;
   try {
    write = new FileOutputStream(merge);
    //依次读取这个文件
    for (File file : files) {
     //读取分块文件的流对象
     read = new FileInputStream(file);
     //先读后写
     while ((len = read.read(bytes)) != -1){
      write.write(bytes,0,len);
     }
    }
   } catch (IOException e) {
    e.printStackTrace();
   } finally {
    try {
     if (read != null){
      read.close();
     }
    } catch (IOException e) {
     e.printStackTrace();
    }
    try {
     if (write != null){
      write.close();
     }
    } catch (IOException e) {
     e.printStackTrace();
    }
   }

   //校验合并后的文件是否正确
   String md5Hex1 = null;
   try {
    FileInputStream mergeFileStream = new FileInputStream(merge);
    md5Hex1 = DigestUtils.md5Hex(mergeFileStream);
   } catch (IOException e) {
    e.printStackTrace();
   }
   if (!fileMd5.equals(md5Hex1)){
    log.debug("合并文件校验不通过，文件路径：{}，原始文件md5：{}",merge.getAbsolutePath(),fileMd5);
    throw new XueChengPlusException("合并文件校验失败！");
   }
   //将合并后的文件上传到文件系统
   //拿到合并文件在MINIO的存储路径
   String filePathByMd5 = getFilePathByMd5(fileMd5, extension);
   addMediaFilesToMinIO(merge.getAbsolutePath(),bucket_videofiles,filePathByMd5);
   //将文件信息入库保存
   uploadFileParamsDto.setFileSize(merge.length()); //合并文件的大小
   addMedisFilesToDb(companyId,fileMd5,uploadFileParamsDto,bucket_videofiles,filePathByMd5);
   return RestResponse.success(true);
  } finally {
   //删除临时分块文件
   if (files != null){
    for (File file : files) {
     if (file.exists()){
      file.delete();
     }
    }
   }
   //删除合并的临时文件
   if (merge != null){
    merge.delete();
   }
  }

 }

 //合并文件在MINIO的存储路径
 private String getFilePathByMd5(String fileMd5,String fileExt){
  return fileMd5.substring(0,1) + "/" + fileMd5.substring(1,2) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
 }


 //下载分块
 private File[] checkChunkStatus(String fileMd5, int chunkTotal){

  //得到分块文件的目录路径
  String chunkFileFolderPath = getChunkFileFolderPath(fileMd5);
  //所有分块文件列表
  File[] files = new File[chunkTotal];
  //开始下载
  for (int i = 0; i < chunkTotal; i++) {
   //得到分块文件的路径
   String chunkFilePath = chunkFileFolderPath + i;
   //在磁盘上创建一个临时文件
   File chunkFile = null;
   try {
    chunkFile = File.createTempFile("chunk",null);
   } catch (Exception e){
    e.printStackTrace();
    XueChengPlusException.cast("创建临时文件出错:{}" + e.getMessage());
   }
 //下载分块文件
   downloadFileFromMinIO(chunkFile, bucket_videofiles, chunkFilePath);
   //将分块文件加入数组
   files[i] = chunkFile;
//   GetObjectArgs build = GetObjectArgs.builder().bucket(bucket_videofiles).object(chunkFilePath).build();
//   try {
//    FilterInputStream inputStream = minioClient.getObject(build);
//    FileOutputStream outputStream = new FileOutputStream(chunkFile);
//    IOUtils.copy(inputStream,outputStream);
//    //将分块文件加入数组
//    files[i] = chunkFile;
//   }catch (Exception e){
//    e.printStackTrace();
//    XueChengPlusException.cast("查询分块文件出错！");
//   }

  }

  return files;
 }

 //根据桶和文件路径从minio下载文件
 public File downloadFileFromMinIO(File file,String bucket,String objectName){

  GetObjectArgs build = GetObjectArgs.builder().bucket(bucket).object(objectName).build();
  try {
   FilterInputStream inputStream = minioClient.getObject(build);
   FileOutputStream outputStream = new FileOutputStream(file);
   IOUtils.copy(inputStream,outputStream);
   return file;
  }catch (Exception e){
   e.printStackTrace();
   throw new XueChengPlusException("下载错误");
  }
 }


 //根据id查询文件的信息
 @Override
 public MediaFiles getFileById(String id) {
  MediaFiles mediaFiles = mediaFilesMapper.selectById(id);
  if (mediaFiles == null){
   XueChengPlusException.cast("文件不存在");
  }
  String url = mediaFiles.getUrl();
  if (StringUtils.isEmpty(url)){
   XueChengPlusException.cast("文件还没有处理，请稍后处理！");
  }
  return mediaFiles;
 }
}
